package cn.micro.eye.commons.auth;

import cn.micro.eye.commons.configuration.MicroProperties;
import cn.micro.eye.commons.exception.MicroBadRequestException;
import cn.micro.eye.commons.exception.MicroPermissionException;
import cn.micro.eye.entity.Permission;
import cn.micro.eye.entity.Role;
import cn.micro.eye.entity.RolePermission;
import cn.micro.eye.mapper.IPermissionMapper;
import cn.micro.eye.mapper.IRoleMapper;
import cn.micro.eye.mapper.IRolePermissionMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Auth Handler Interceptor
 *
 * @author lry
 */
@Slf4j
@Order(1)
@Configuration
public class GlobalAuthHandlerInterceptor extends HandlerInterceptorAdapter implements InitializingBean, DisposableBean {

    private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
    private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = null;
    private final ConcurrentMap<String, List<Permission>> rolePermissions = new ConcurrentHashMap<>();
    private final Object LOCK = new Object();

    @Resource
    private MicroProperties properties;
    @Resource
    private IRoleMapper roleMapper;
    @Resource
    private IPermissionMapper permissionMapper;
    @Resource
    private IRolePermissionMapper rolePermissionMapper;

    @Override
    public void afterPropertiesSet() throws Exception {
        if (!properties.isAuthRefresh()) {
            this.refresh();
            return;
        }

        this.scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("global-auth-task-executor");
            return t;
        });
        scheduledThreadPoolExecutor.scheduleWithFixedDelay(this::refresh,
                0, properties.getAuthRefreshSec(), TimeUnit.SECONDS);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // Authorization in Header is automatically parsed and filled.
        String accessTokenValue = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (!(accessTokenValue == null || accessTokenValue.length() == 0)) {
            if (accessTokenValue.indexOf(" ") > 0) {
                accessTokenValue = accessTokenValue.substring(accessTokenValue.indexOf(" ") + 1);
            } else {
                throw new MicroBadRequestException("Illegal authorization parameter");
            }
        }
        // X-Access-Token in Header is automatically parsed and filled.
        if (accessTokenValue == null || accessTokenValue.length() == 0) {
            accessTokenValue = request.getHeader(AuthContext.ACCESS_TOKEN_HEADER_KEY);
        }
        // access_token in Query is automatically parsed and filled.
        if (accessTokenValue == null || accessTokenValue.length() == 0) {
            accessTokenValue = request.getParameter(AuthContext.ACCESS_TOKEN_QUERY_KEY);
        }
        // Parse and set to Context
        if (!(accessTokenValue == null || accessTokenValue.length() == 0)) {
            request.setAttribute(AuthContext.ACCESS_TOKEN_KEY, AuthContext.parseToken(accessTokenValue));
        }

        // === Check CORS
        if (RequestMethod.OPTIONS.name().equals(request.getMethod())) {
            return true;
        }

        // === Check HandlerMethod
        if (!(handler instanceof HandlerMethod)) {
            if (!(handler instanceof ResourceHttpRequestHandler)) {
                log.warn("Illegal Handler：{}", handler);
            }

            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;

        // === Check Handler PreAuth
        boolean needVerifyToken = false;
        PreAuth methodPreAuth = handlerMethod.getMethodAnnotation(PreAuth.class);
        PreAuth beanPreAuth = handlerMethod.getBeanType().getAnnotation(PreAuth.class);
        if (methodPreAuth != null || beanPreAuth != null) {
            needVerifyToken = true;
        }
        NonAuth methodNonAuth = handlerMethod.getMethodAnnotation(NonAuth.class);
        NonAuth beanNonAuth = handlerMethod.getBeanType().getAnnotation(NonAuth.class);
        if (methodNonAuth != null || beanNonAuth != null) {
            needVerifyToken = false;
        }
        if (!needVerifyToken) {
            return true;
        }
        AuthContext.verify(accessTokenValue);

        // === Check Role Permission
        TokenBody tokenBody = AuthContext.getContextAccessToken();
        if (tokenBody.getAuthorities() == null || tokenBody.getAuthorities().isEmpty()) {
            throw new MicroPermissionException("Unassigned role");
        }
        String servletPath = request.getServletPath();
        for (String role : tokenBody.getAuthorities()) {
            List<Permission> permissions = rolePermissions.get(role);
            if (permissions == null || permissions.isEmpty()) {
                continue;
            }

            for (Permission permission : permissions) {
                if (HttpMethod.resolve(permission.getMethod()) == null) {
                    continue;
                }
                if (!permission.getMethod().equals(request.getMethod())) {
                    continue;
                }
                if (ANT_PATH_MATCHER.match(permission.getUrl(), servletPath)) {
                    return super.preHandle(request, response, handler);
                }
            }
        }

        throw new MicroPermissionException("Roles unauthorized");
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }

    @Override
    public void destroy() throws Exception {
        if (scheduledThreadPoolExecutor != null) {
            scheduledThreadPoolExecutor.shutdown();
        }
    }

    private void refresh() {
        // select all role
        List<Role> roleList = roleMapper.selectList(new QueryWrapper<>());

        // select all permission
        List<Permission> permissionList = permissionMapper.selectList(new QueryWrapper<>());
        Map<Long, Permission> permissionMap = new HashMap<>();
        if (!(permissionList == null || permissionList.isEmpty())) {
            permissionMap.putAll(permissionList.stream().collect(Collectors.toMap(Permission::getId, Function.identity())));
        }

        // select all role_permission
        List<RolePermission> rolePermissionList = rolePermissionMapper.selectList(new QueryWrapper<>());
        Map<Long, List<RolePermission>> rolePermissionMap = new HashMap<>();
        if (!(rolePermissionList == null || rolePermissionList.isEmpty())) {
            for (RolePermission rp : rolePermissionList) {
                rolePermissionMap.computeIfAbsent(rp.getRoleId(), k -> new ArrayList<>()).add(rp);
            }
        }

        ConcurrentMap<String, List<Permission>> tempRolePermissions = new ConcurrentHashMap<>();

        // calculation role->permission collection
        for (Role role : roleList) {
            List<Permission> tempPermissionList = tempRolePermissions.computeIfAbsent(role.getCode(), k -> new ArrayList<>());
            List<RolePermission> tempRolePermissionList = rolePermissionMap.get(role.getId());
            if (tempRolePermissionList == null) {
                continue;
            }

            for (RolePermission rp : tempRolePermissionList) {
                Permission tempPermission = permissionMap.get(rp.getPermissionId());
                if (tempPermission == null) {
                    continue;
                }
                if (StringUtils.isEmpty(tempPermission.getUrl()) ||
                        StringUtils.isEmpty(tempPermission.getMethod()) ||
                        HttpMethod.resolve(tempPermission.getMethod()) == null) {
                    continue;
                }
                tempPermissionList.add(tempPermission);
            }
        }

        synchronized (LOCK) {
            rolePermissions.clear();
            rolePermissions.putAll(tempRolePermissions);
        }
    }

}